﻿using GTM = Gadgeteer.Modules;
using GTI = Gadgeteer.Interfaces;
using System.Threading;
using System;
using Microsoft.SPOT;

// Driver contributed by TinyCLR communitity members Eduardo Velloso (@eduardo.velloso) and Brett Pound (@Brett).
namespace Gadgeteer.Modules.GHIElectronics
{
    // -- CHANGE FOR MICRO FRAMEWORK 4.2 --
    // If you want to use Serial, SPI, or DaisyLink (which includes GTI.SoftwareI2C), you must do a few more steps
    // since these have been moved to separate assemblies for NETMF 4.2 (to reduce the minimum memory footprint of Gadgeteer)
    // 1) add a reference to the assembly (named Gadgeteer.[interfacename])
    // 2) in GadgeteerHardware.xml, uncomment the lines under <Assemblies> so that end user apps using this module also add a reference.

    /// <summary>
    /// A Bluetooth module for Microsoft .NET Gadgeteer
    /// </summary>
    public class Bluetooth : GTM.Module
    {

        /// <summary>
        /// Direct access to Serial Port.
        /// </summary>
        public GTI.Serial serialPort;
        private GTI.DigitalOutput reset;
        private GTI.InterruptInput statusInt;
        private Thread readerThread;

        /// <summary>
        /// Possible states of the Bluetooth module
        /// </summary>
        public enum BluetoothState
        {
            /// <summary>
            /// Module is initializing
            /// </summary>
            Initializing = 0,
            /// <summary>
            /// Module is ready
            /// </summary>
            Ready = 1,
            /// <summary>
            /// Module is in pairing mode
            /// </summary>
            Inquiring = 2,
            /// <summary>
            /// Module is making a connection attempt
            /// </summary>
            Connecting = 3,
            /// <summary>
            /// Module is connected
            /// </summary>
            Connected = 4,
            /// <summary>
            /// Module is diconnected
            /// </summary>
            Disconnected = 5
        }

        // Note: A constructor summary is auto-generated by the doc builder.
        /// <summary></summary>
        /// <param name="socketNumber">The socket that this module is plugged in to.</param>
        public Bluetooth(int socketNumber)
        {
            // This finds the Socket instance from the user-specified socket number.  
            // This will generate user-friendly error messages if the socket is invalid.
            // If there is more than one socket on this module, then instead of "null" for the last parameter, 
            // put text that identifies the socket to the user (e.g. "S" if there is a socket type S)
            Socket socket = Socket.GetSocket(socketNumber, true, this, null);

            this.reset = new GTI.DigitalOutput(socket, Socket.Pin.Six, false, this);
            this.statusInt = new GTI.InterruptInput(socket, Socket.Pin.Three, GTI.GlitchFilterMode.Off, GTI.ResistorMode.Disabled, GTI.InterruptMode.RisingAndFallingEdge, this);
            this.serialPort = new GTI.Serial(socket, 38400, GTI.Serial.SerialParity.None, GTI.Serial.SerialStopBits.One, 8, GTI.Serial.HardwareFlowControl.NotRequired, this);

            //this.statusInt.Interrupt += new GTI.InterruptInput.InterruptEventHandler(statusInt_Interrupt);
            this.serialPort.ReadTimeout = Timeout.Infinite;
            this.serialPort.Open();

            Thread.Sleep(5);
            this.reset.Write(true);

            readerThread = new Thread(new ThreadStart(runReaderThread));
            readerThread.Start();
            Thread.Sleep(500);
        }

        // Note: A constructor summary is auto-generated by the doc builder.
        /// <summary></summary>
        /// <param name="socketNumber">The socket that this module is plugged in to.</param>
        /// <param name="baud">The baud rate to communicate with the module with.</param>
        public Bluetooth(int socketNumber, long baud)
        {
            // This finds the Socket instance from the user-specified socket number.  
            // This will generate user-friendly error messages if the socket is invalid.
            // If there is more than one socket on this module, then instead of "null" for the last parameter, 
            // put text that identifies the socket to the user (e.g. "S" if there is a socket type S)
            Socket socket = Socket.GetSocket(socketNumber, true, this, null);

            this.reset = new GTI.DigitalOutput(socket, Socket.Pin.Six, false, this);
            this.statusInt = new GTI.InterruptInput(socket, Socket.Pin.Three, GTI.GlitchFilterMode.Off, GTI.ResistorMode.Disabled, GTI.InterruptMode.RisingAndFallingEdge, this);
            this.serialPort = new GTI.Serial(socket, 38400, GTI.Serial.SerialParity.None, GTI.Serial.SerialStopBits.One, 8, GTI.Serial.HardwareFlowControl.NotRequired, this);

            //this.statusInt.Interrupt += new GTI.InterruptInput.InterruptEventHandler(statusInt_Interrupt);
            this.serialPort.ReadTimeout = Timeout.Infinite;
            this.serialPort.Open();

            Thread.Sleep(5);
            this.reset.Write(true);

            // Poundy added:
            Thread.Sleep(5);
            this.SetDeviceBaud(baud);
            this.serialPort.Flush();
            this.serialPort.Close();
            this.serialPort.BaudRate = (int)baud;
            this.serialPort.Open();
            // Poundy 

            readerThread = new Thread(new ThreadStart(runReaderThread));
            readerThread.Start();
            Thread.Sleep(500);
        }

        /// <summary>
        /// Hard Reset Bluetooth module
        /// </summary>
        public void Reset()
        {
            this.reset.Write(false);
            Thread.Sleep(5);
            this.reset.Write(true);
        }

        /// <summary>
        /// Gets a value that indicates whether the bluetooth connection is connected.
        /// </summary>
        public bool IsConnected
        {
            get
            {
                return this.statusInt.Read();
            }
        }


        /// <summary>
        /// Thread that continuously reads incoming messages from the module,
        /// parses them and triggers the corresponding events.
        /// </summary>
        private void runReaderThread()
        {
            Debug.Print("Reader Thread");
            while (true)
            {
                String response = "";
                while (serialPort.BytesToRead > 0)
                {
                    response = response + (char)serialPort.ReadByte();
                }
                if (response.Length > 0)
                {
                    Debug.Print(response);


                    //Check Bluetooth State Changed
                    if (response.IndexOf("+BTSTATE:") > -1)
                    {
                        string atCommand = "+BTSTATE:";

                        //String parsing  
                        // Return format: +COPS:<mode>[,<format>,<oper>]
                        int first = response.IndexOf(atCommand) + atCommand.Length;
                        int last = response.IndexOf("\n", first);
                        int state = int.Parse(((response.Substring(first, last - first)).Trim()));

                        OnBluetoothStateChanged(this, (BluetoothState)state);
                    }
                    //Check Pin Requested
                    if (response.IndexOf("+INPIN") > -1)
                    {
                        // EDUARDO : Needs testing
                        OnPinRequested(this);
                    }
                    if (response.IndexOf("+RTINQ") > -1)
                    {
                        //EDUARDO: Needs testing

                        string atCommand = "+RTINQ=";
                        //String parsing  
                        int first = response.IndexOf(atCommand) + atCommand.Length;
                        int mid = response.IndexOf(";", first);
                        int last = response.IndexOf("\r", first);

                        // Keep reading until the end of the message
                        while (last < 0)
                        {
                            while (serialPort.BytesToRead > 0)
                            {
                                response = response + (char)serialPort.ReadByte();
                            }
                            last = response.IndexOf("\r", first);
                        }

                        string address = ((response.Substring(first, mid - first)).Trim());

                        string name = (response.Substring(mid + 1, last - mid));

                        OnDeviceInquired(this, address, name);
                        //Debug.Print("Add: " + address + ", Name: " + name );
                    }
                    else
                    {
                        OnDataReceived(this, response);
                    }
                }
                Thread.Sleep(1);  //poundy changed from thread.sleep(10)
            }
        }

        #region DELEGATES AND EVENTS
        #region Bluetooth State Changed
        /// <summary>
        /// Represents the delegate used for the <see cref="BluetoothStateChanged"/> event.
        /// </summary>
        /// <param name="sender">The object that raised the event.</param>
        /// <param name="btState">Current state of the Bluetooth module</param>
        public delegate void BluetoothStateChangedHandler(Bluetooth sender, BluetoothState btState);
        /// <summary>
        /// Event raised when the bluetooth module changes its state.
        /// </summary>
        public event BluetoothStateChangedHandler BluetoothStateChanged;
        private BluetoothStateChangedHandler onBluetoothStateChanged;

        /// <summary>
        /// Raises the <see cref="BluetoothStateChanged"/> event.
        /// </summary>
        /// <param name="sender">The object that raised the event.</param>  
        /// <param name="btState">Current state of the Bluetooth module</param>
        protected virtual void OnBluetoothStateChanged(Bluetooth sender, BluetoothState btState)
        {
            if (onBluetoothStateChanged == null) onBluetoothStateChanged = new BluetoothStateChangedHandler(OnBluetoothStateChanged);
            if (Program.CheckAndInvoke(BluetoothStateChanged, onBluetoothStateChanged, sender, btState))
            {
                BluetoothStateChanged(sender, btState);
            }
        }
        #endregion
        #region DataReceived
        /// <summary>
        /// Represents the delegate used for the <see cref="DataReceived"/> event.
        /// </summary>
        /// <param name="sender">The object that raised the event.</param>
        /// <param name="data">Data received from the Bluetooth module</param>
        public delegate void DataReceivedHandler(Bluetooth sender, string data);
        /// <summary>
        /// Event raised when the bluetooth module changes its state.
        /// </summary>
        public event DataReceivedHandler DataReceived;
        private DataReceivedHandler onDataReceived;

        /// <summary>
        /// Raises the <see cref="DataReceived"/> event.
        /// </summary>
        /// <param name="sender">The object that raised the event.</param>  
        /// <param name="data">Data string received by the Bluetooth module</param>
        protected virtual void OnDataReceived(Bluetooth sender, string data)
        {
            if (onDataReceived == null) onDataReceived = new DataReceivedHandler(OnDataReceived);
            if (Program.CheckAndInvoke(DataReceived, onDataReceived, sender, data))
            {
                DataReceived(sender, data);
            }
        }
        #endregion
        #region PinRequested
        /// <summary>
        /// Represents the delegate used for the <see cref="PinRequested"/> event.
        /// </summary>
        /// <param name="sender">The object that raised the event.</param>
        public delegate void PinRequestedHandler(Bluetooth sender);
        /// <summary>
        /// Event raised when the bluetooth module changes its state.
        /// </summary>
        public event PinRequestedHandler PinRequested;
        private PinRequestedHandler onPinRequested;

        /// <summary>
        /// Raises the <see cref="PinRequested"/> event.
        /// </summary>
        /// <param name="sender">The object that raised the event.</param>  
        protected virtual void OnPinRequested(Bluetooth sender)
        {
            if (onPinRequested == null) onPinRequested = new PinRequestedHandler(OnPinRequested);
            if (Program.CheckAndInvoke(PinRequested, onPinRequested, sender))
            {
                PinRequested(sender);
            }
        }
        #endregion
        #endregion

        #region DeviceInquired
        /// <summary>
        /// Represents the delegate used for the <see cref="DeviceInquired"/> event.
        /// </summary>
        /// <param name="sender">The object that raised the event.</param>
        /// <param name="macAddress">MAC Address of the inquired device</param>
        /// <param name="name">Name of the inquired device</param>
        public delegate void DeviceInquiredHandler(Bluetooth sender, string macAddress, string name);
        /// <summary>
        /// Event raised when the bluetooth module changes its state.
        /// </summary>
        public event DeviceInquiredHandler DeviceInquired;
        private DeviceInquiredHandler onDeviceInquired;

        /// <summary>
        /// Raises the <see cref="PinRequested"/> event.
        /// </summary>
        /// <param name="sender">The object that raised the event.</param>  
        /// <param name="macAddress">MAC Address of the inquired device</param>
        /// <param name="name">Name of the inquired device</param>
        protected virtual void OnDeviceInquired(Bluetooth sender, string macAddress, string name)
        {
            if (onDeviceInquired == null) onDeviceInquired = new DeviceInquiredHandler(OnDeviceInquired);
            if (Program.CheckAndInvoke(DeviceInquired, onDeviceInquired, sender, macAddress, name))
            {
                DeviceInquired(sender, macAddress, name);
            }
        }
        #endregion
        private object _lock = new Object();

        private Client _client;
        /// <summary>
        /// Sets Bluetooth module to work in Client mode.
        /// </summary>
        public Client ClientMode
        {
            get
            {
                lock (_lock)
                {
                    if (_host != null) throw new InvalidOperationException("Cannot use both Client and Host modes for Bluetooth module");
                    if (_client == null) _client = new Client(this);
                    return _client;
                }
            }
        }

        private Host _host;
        /// <summary>
        /// Sets Bluetooth module to work in Host mode.
        /// </summary>
        public Host HostMode
        {
            get
            {
                lock (_lock)
                {
                    if (_client != null) throw new InvalidOperationException("Cannot use both Client and Host modes for Bluetooth module");
                    if (_host == null) _host = new Host(this);
                    return _host;
                }
            }
        }

        /// <summary>
        /// Sets the device name as seen by other devices
        /// </summary>
        /// <param name="name">Name of the device</param>
        public void SetDeviceName(string name)
        {
            this.serialPort.Write("\r\n+STNA=" + name + "\r\n");
        }

        /// <summary>
        /// Switch the device to the directed speed
        /// </summary>
        /// <param baud="number">Name of the device</param>
        public void SetDeviceBaud(long baud)
        {
            string cmd = "";
            switch (baud)
            {
                case 9600:
                    cmd = "9600";
                    break;
                case 19200:
                    cmd = "19200";
                    break;
                case 38400:
                    cmd = "38400";
                    break;
                case 57600:
                    cmd = "57600";
                    break;
                case 115200:
                    cmd = "115200";
                    break;
                case 230400:
                    cmd = "230400";
                    break;
                case 460800:
                    cmd = "460800";
                    break;
                default:
                    cmd = "";
                    break;
            }

            if (cmd != "")
                this.serialPort.Write("\r\n+STBD=" + cmd + "\r\n");
            //todo: check it is working?! Probably should check the return code and do something about it. in the meantime,
            Thread.Sleep(500);
        }

        /// <summary>
        /// Sets the PIN code for the Bluetooth module
        /// </summary>
        /// <param name="pinCode"></param>
        public void SetPinCode(string pinCode)
        {
            this.serialPort.Write("\r\n+STPIN=" + pinCode + "\r\n");
        }

        /// <summary>
        /// Client functionality for the Bluetooth module
        /// </summary>
        public class Client
        {
            private Bluetooth bluetooth;
            internal Client(Bluetooth bluetooth)
            {
                Debug.Print("Client Mode");
                this.bluetooth = bluetooth;
                bluetooth.serialPort.Write("\r\n+STWMOD=0\r\n");
            }

            /// <summary>
            /// Enters pairing mode
            /// </summary>
            public void EnterPairingMode()
            {
                Debug.Print("Enter Pairing Mode");
                bluetooth.serialPort.Write("\r\n+INQ=1\r\n");

            }

            /// <summary>
            /// Inputs pin code
            /// </summary>
            /// <param name="pinCode">Module's pin code. Default: 0000</param>
            public void InputPinCode(string pinCode)
            {
                Debug.Print("Inputting pin: " + pinCode);
                bluetooth.serialPort.Write("\r\n+RTPIN=" + pinCode + "\r\n");
            }

            /// <summary>
            /// Closes current connection. Doesn't work yet.
            /// </summary>
            public void Disconnect()
            {
                Debug.Print("Disconnection is not working...");
                //NOT WORKING
                // Documentation states that in order to disconnect, we pull PIO0 HIGH,
                // but this pin is not available in the socket... (see schematics)
            }

            /// <summary>
            /// Sends data through the connection.
            /// </summary>
            /// <param name="message">String containing the data to be sent</param>
            public void Send(string message)
            {
                Debug.Print("Sending: " + message);
                bluetooth.serialPort.Write(message);
            }

            /// <summary>
            /// Sends data through the connection.
            /// </summary>
            /// <param name="message">String containing the data to be sent</param>
            public void SendLine(string message)
            {
                Debug.Print("Sending: " + message);
                bluetooth.serialPort.WriteLine(message);
            }
        }

        /// <summary>
        /// Implements the host functionality for the Bluetooth module
        /// </summary>
        public class Host
        {
            private Bluetooth bluetooth;
            internal Host(Bluetooth bluetooth)
            {
                Debug.Print("Host mode");
                this.bluetooth = bluetooth;
                bluetooth.serialPort.Write("\r\n+STWMOD=1\r\n");
            }

            /// <summary>
            /// Starts inquiring for devices
            /// </summary>
            public void InquireDevice()
            {
                Debug.Print("Inquiring device");
                bluetooth.serialPort.Write("\r\n+INQ=1\r\n");

            }

            /// <summary>
            /// Makes a connection with a device using its MAC address.
            /// </summary>
            /// <param name="macAddress">MAC address of the device</param>
            public void Connect(string macAddress)
            {
                Debug.Print("Connecting to: " + macAddress);
                bluetooth.serialPort.Write("\r\n+CONN=" + macAddress + "\r\n");
            }

            /// <summary>
            /// Inputs the PIN code.
            /// </summary>
            /// <param name="pinCode">PIN code. Default 0000</param>
            public void InputPinCode(string pinCode)
            {
                Debug.Print("Inputting pin: " + pinCode);
                bluetooth.serialPort.Write("\r\n+RTPIN=" + pinCode + "\r\n");
            }

            /// <summary>
            /// Closes the current connection. Doesn't work yet.
            /// </summary>
            public void Disconnect()
            {
                Debug.Print("Disconnection is not working...");
                //NOT WORKING
                // Documentation states that in order to disconnect, we pull PIO0 HIGH,
                // but this pin is not available in the socket... (see schematics)
            }
        }


    }
}

